import { Store } from "../../core";

import { HIGHLIGHT_LAYER_ATTR, HIGHLIGHT_PAGE_ATTR } from "./constants";
import getRectFromOffsets from "./getRectFromOffsets";
import getTextFromOffsets from "./getTextFromOffsets";
import {
  NO_SELECTION_STATE,
  SELECTING_STATE,
  SelectedState,
} from "./SelectionState";
import StoreProps from "./StoreProps";
import { transformArea } from "./transformArea";
import HighlightArea from "./types/HighlightArea";
import SelectionData from "./types/SelectionData";
import SelectionRange from "./types/SelectionRange";
import useRotation from "./useRotation";
import { FC, useEffect, useRef, useState } from "react";

const Tracker: FC<{
  store: Store<StoreProps>;
}> = ({ store }) => {
  const { rotation } = useRotation(store);
  const pagesRef = useRef<HTMLElement | null>(null);
  const [arePagesFound, setPagesFound] = useState(false);

  const handlePagesContainer = (getPagesContainer: () => HTMLElement) => {
    const ele = getPagesContainer();
    pagesRef.current = ele;
    setPagesFound(!!ele);
  };

  const onMouseUpHandler = () => {
    // Get the current selection
    const selection = document.getSelection() as Selection;

    const selectionState = store.get("selectionState");
    const hasSelection =
      (selectionState === NO_SELECTION_STATE ||
        selectionState === SELECTING_STATE) &&
      selection &&
      selection.rangeCount > 0 &&
      selection.toString() !== "";
    if (!hasSelection) {
      return;
    }

    const range = selection.getRangeAt(0);
    const startDiv = range.startContainer.parentNode;
    const parentEndContainer = range.endContainer.parentNode;
    const shouldIgnoreEndContainer =
      parentEndContainer instanceof HTMLElement &&
      parentEndContainer.hasAttribute(HIGHLIGHT_LAYER_ATTR);

    let endDiv: Node | ChildNode | null;
    let endOffset: number;
    if (shouldIgnoreEndContainer && range.endOffset == 0) {
      endDiv = range.endContainer.previousSibling;
      endOffset = endDiv?.textContent?.length || 0;
    } else if (shouldIgnoreEndContainer) {
      endDiv = range.endContainer;
      endOffset = range.endOffset;
    } else {
      endDiv = parentEndContainer;
      endOffset = range.endOffset;
    }

    if (
      !(startDiv instanceof HTMLElement) ||
      !(endDiv instanceof HTMLElement)
    ) {
      return;
    }

    const startPageIdx = parseInt(
      startDiv.getAttribute(HIGHLIGHT_PAGE_ATTR) as string,
      10
    );
    const endPageIdx = parseInt(
      endDiv.getAttribute(HIGHLIGHT_PAGE_ATTR) as string,
      10
    );

    const startTextLayer = startDiv.parentElement;
    const endTextLayer = endDiv.parentElement;

    const startPageRect = startTextLayer?.getBoundingClientRect();
    const startDivSiblings: HTMLElement[] = [].slice.call(
      startTextLayer?.querySelectorAll(`[${HIGHLIGHT_PAGE_ATTR}]`)
    );
    const startDivIdx = startDivSiblings.indexOf(startDiv);

    const endPageRect = endTextLayer?.getBoundingClientRect();
    const endDivSiblings: HTMLElement[] = [].slice.call(
      endTextLayer?.querySelectorAll(`[${HIGHLIGHT_PAGE_ATTR}]`)
    );
    const endDivIdx = endDivSiblings.indexOf(endDiv);

    let rangeType: SelectionRange = SelectionRange.DifferentPages;
    switch (true) {
      case startPageIdx === endPageIdx && startDivIdx === endDivIdx:
        rangeType = SelectionRange.SameDiv;
        break;
      case startPageIdx === endPageIdx && startDivIdx < endDivIdx:
        rangeType = SelectionRange.DifferentDivs;
        break;
      default:
        rangeType = SelectionRange.DifferentPages;
        break;
    }

    const getRectBetween = (
      min: number,
      max: number,
      eleArray: HTMLElement[]
    ) =>
      Array(max - min + 1)
        .fill(0)
        .map((_, i) => eleArray[min + i].getBoundingClientRect());

    let highlightAreas: HighlightArea[] = [];

    const startDivEndOffset = startDiv?.textContent?.length || 0;
    const {
      width: startWidth,
      height: startHeight,
      left: startLeft,
      top: startTop,
    } = startPageRect as DOMRect;

    const {
      width: endWidth,
      height: endHeight,
      top: endTop,
      left: endLeft,
    } = endPageRect as DOMRect;

    switch (rangeType) {
      case SelectionRange.SameDiv: {
        // eslint-disable-next-line no-case-declarations
        const rect = getRectFromOffsets(startDiv, range.startOffset, endOffset);
        const { height, width, top, left } = rect;
        highlightAreas = [
          {
            height: (height * 100) / startHeight,
            left: ((left - startLeft) * 100) / startWidth,
            pageIndex: startPageIdx,
            top: ((top - startTop) * 100) / startHeight,
            width: (width * 100) / startWidth,
          },
        ];
        break;
      }

      case SelectionRange.DifferentDivs: {
        highlightAreas = [
          getRectFromOffsets(startDiv, range.startOffset, startDivEndOffset),
        ]
          .concat(
            getRectBetween(startDivIdx + 1, endDivIdx - 1, startDivSiblings)
          )
          .concat([getRectFromOffsets(endDiv, 0, endOffset)])
          .map((rect) => {
            const { height, width, top, left } = rect;
            return {
              height: (height * 100) / startHeight,
              left: ((left - startLeft) * 100) / startWidth,
              pageIndex: startPageIdx,
              top: ((top - startTop) * 100) / startHeight,
              width: (width * 100) / startWidth,
            };
          });
        break;
      }

      case SelectionRange.DifferentPages: {
        // eslint-disable-next-line no-case-declarations
        const startAreas = [
          getRectFromOffsets(startDiv, range.startOffset, startDivEndOffset),
        ]
          .concat(
            getRectBetween(
              startDivIdx + 1,
              startDivSiblings.length - 1,
              startDivSiblings
            )
          )
          .map((rect) => {
            const { height, width, top, left } = rect;
            return {
              height: (height * 100) / startHeight,
              left: ((left - startLeft) * 100) / startWidth,
              pageIndex: startPageIdx,
              top: ((top - startTop) * 100) / startHeight,
              width: (width * 100) / startWidth,
            };
          });

        // eslint-disable-next-line no-case-declarations
        const endAreas = getRectBetween(0, endDivIdx - 1, endDivSiblings)
          .concat([getRectFromOffsets(endDiv, 0, endOffset)])
          .map((rect) => {
            const { height, width, top, left } = rect;
            return {
              height: (height * 100) / endHeight,
              left: ((left - endLeft) * 100) / endWidth,
              pageIndex: endPageIdx,
              top: ((top - endTop) * 100) / endHeight,
              width: (width * 100) / endWidth,
            };
          });
        highlightAreas = startAreas.concat(endAreas);
        break;
      }
    }

    // Determine the selected text
    let selectedText = "";
    switch (rangeType) {
      case SelectionRange.SameDiv:
        selectedText = getTextFromOffsets(
          startTextLayer,
          startDivIdx,
          range.startOffset,
          startDivIdx,
          endOffset
        );
        break;

      case SelectionRange.DifferentDivs:
        selectedText = getTextFromOffsets(
          startTextLayer,
          startDivIdx,
          range.startOffset,
          endDivIdx,
          endOffset
        );
        break;

      case SelectionRange.DifferentPages:
        // eslint-disable-next-line no-case-declarations
        const startText = getTextFromOffsets(
          startTextLayer,
          startDivIdx,
          range.startOffset,
          startDivSiblings.length
        );

        // eslint-disable-next-line no-case-declarations
        const endText = getTextFromOffsets(
          endTextLayer,
          0,
          0,
          endDivIdx,
          endOffset
        );

        selectedText = `${startText}\n${endText}`;
        break;
    }

    let selectionRegion: HighlightArea;
    if (highlightAreas.length > 0) {
      selectionRegion = highlightAreas[highlightAreas.length - 1];
    } else {
      const { width, left, top, height } = endDiv.getBoundingClientRect();
      selectionRegion = {
        height: (height * 100) / endHeight,
        left: ((left - endLeft) * 100) / endWidth,
        pageIndex: endPageIdx,
        top: ((top - endTop) * 100) / endHeight,
        width: (width * 100) / endWidth,
      };
    }

    const selectionData: SelectionData = {
      startPageIndex: startPageIdx - 1,
      endPageIndex: endPageIdx - 1,
      startOffset: range.startOffset,
      startDivIndex: startDivIdx,
      endOffset,
      endDivIndex: endDivIdx,
    };

    store.update(
      "selectionState",
      new SelectedState(
        selectedText,
        highlightAreas.map((area) => transformArea(area, rotation)),
        selectionData,
        selectionRegion
      )
    );
  };

  useEffect(() => {
    const ele = pagesRef.current;
    if (!ele) {
      return;
    }

    ele.addEventListener("mouseup", onMouseUpHandler);
    return (): void => {
      ele.removeEventListener("mouseup", onMouseUpHandler);
    };
  }, [arePagesFound, rotation]);

  useEffect(() => {
    store.subscribe("getPagesContainer", handlePagesContainer);

    return (): void => {
      store.unsubscribe("getPagesContainer", handlePagesContainer);
    };
  }, []);

  return <></>;
};

export default Tracker;
